感謝 iT 邦幫忙與博碩文化,本系列文章已出版成書「從 Hooks 開始,讓你的網頁 React 起來」,首刷版稅將全額贊助 iT 邦幫忙鐵人賽,歡迎前往購書,鼓勵筆者撰寫更多優質文章。
在 React 18 後已經棄用
ReactDOM.render()
,改用ReactDOM.createRoot()
,內文中的圖片並未一併修改,煩請讀者留意。
⚠️ 警告:密集恐懼症者慎入本篇內容。如果你連 iPhone 11 Pro 的三鏡頭都會覺得不適或感到恐懼的話,可能無法閱讀本文
看到自己桌面上滿滿的計數器截圖,可以感受到讀者一定想說鐵人賽都快三分之一了,怎麼還在計數器,PJ 也太混了吧!為了因應觀眾需求,今天是我們看到計數器的最後一天,就讓我們準備跟它說再見吧。
什麼!準備要跟計數器說再見你覺得有點難過?你怕自己以後忘記怎麼寫計數器,所以覺得要重複一直做這個計數器的練習讓自己更熟練,你打算做 14 次練習,每次都會做一個新的,你心裡 OS:「嘿嘿嘿~練習 14 次總是不會忘記了吧!」。
下圖是你內心想像的畫面:
沒料到,今天的練習要打破你的想法了!(不過如果你還是想要重複寫 14 次也是不反對拉...,熟能生巧是真的)
還記得我們曾經說過,使用元件(component)的好處在於可以快速地重複使用已經寫好的元件,而且每個元件的狀態都是獨立的,也就是說,你不會因為點了「第一個」計數器的向上按鈕,使得剩下其他計數器的數字也都加一,而是只有「第一個」計數器的數字會改變而已。
現在就讓我們開始今天的練習吧,學會今天的內容後,下次碰到需要重複的東西時,相信你都可以很有信心的說:「我要一次打十個!」
在開始戰鬥前,先讓我們整理一下昨天完成的程式碼,因為使用了 visibility: hidden
來隱藏箭頭的樣式比較好看,不會讓畫面排版有抖動的情況,因此就繼續沿用昨天 Day 7 - Counter with useState - conditional inline-style 的這個 CodePen,你可以 Fork 這份程式碼再繼續修改。
在昨天的程式碼中,我們是把使用者點擊按鈕時要做的事直接放在 onClick={}
的 {}
內去執行:
因為這裡 onClick
後只需呼叫 setCount
這個方法,因此並不會有什麼大問題,但若現在 onClick
後需要做更多的事情時,直接把這個事件處理器(event handlers)寫在 JSX 的行內可能就會變得比較難管理。因此,為了程式碼管理上的方便,有時會把事件處理器先定義成一個函式,在 onClick
後去呼叫這個函式即可。
可以把 onClick
裡面的函式拉出來,分別取名為 handleIncrement
和 handleDecrement
,像是這樣:
// ...
const Counter = () => {
const [count, setCount] = useState(5);
const handleIncrement = () => {
setCount(count + 1);
};
const handleDecrement = () => {
setCount(count - 1);
};
return (
// ...
);
};
由於目前這兩個函式裡面並沒有做任何其他的操作,因此在箭頭函式中可以在 =>
後直接呼叫 setCount
方法,就可以把上面的內容精簡成:
// ...
const Counter = () => {
const [count, setCount] = useState(5);
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
// ...
);
};
最後在把 handleIncrement
和 handleDecrement
放到 onClick
內的 {}
內即可。
在功能不變的情況下,整個程式碼又變的更精簡:
完整的程式碼可參考 Day 8 - Multiple Counters - clean code with event handlers @ CodePen
雖然現在程式碼看起來又乾淨了不少,但你可能會想說,看起來 handleIncrement
和 handleDecrement
做的事好像差不多,那可不可以把它們包在一起,寫成一個稱作 handleClick
的函式,接著 handleClick
中帶入一個名為 type
的參數,當 type
為 increment
的時候就呼叫 setCount(count + 1)
;當 type
為 decrement
的時候就呼叫 setCount(count - 1)
,像是這樣:
const Counter = () => {
const [count, setCount] = useState(5);
const handleClick = (type) => {
if (type === 'increment') {
setCount(count + 1);
}
if (type === 'decrement') {
setCount(count - 1);
}
};
return (
// ...
);
};
這麼做當然也是可以的,但要特別留意在放入 onClick
中的內容,很多時候因為在呼叫 handleClick
這個方法的同時又要帶入參數,在 onClick
的地方一不小心可能會寫成這樣 onClick={handleClick('increment')}
:
但當你這麼做時,程式是無法正確執行的:
為什麼錯誤訊息會炸的亂七八糟呢?當我們寫成 onClick={handleClick('increment')}
時,是什麼意思呢?
和剛剛我們寫 onClick={handleIncrement}
不同,當我們寫 onClick={handleClick('increment')}
時,我們預期的的是「當使用者點擊按鈕時,會去執行 handleClick('increment')
這個方法」。但實際上,因為 handleClick
後面直接加上了小括號 ('increment')
,因此當 JavaScript 執行到這裡的時候,這個 handleClick
函式就已經被執行了!
所以實際上畫面在轉譯的時候,就執行了 handleClick
這個函式,這時候就呼叫到了 setCount()
;當 setCount
被呼叫到時,React 發現就會去檢查 count
的值,發現 count
不一樣之後,又會去更新畫面,於是就進入了無限迴圈...
這也就是為什麼在錯誤訊息中會看到「Uncaught Invariant Violation: Too many re-renders. React limits the number of renders to prevent an infinite loop.」,因為它陷入無窮迴圈,畫面一直重複轉譯。
要解決這樣的問題需要把 handleClick()
包在一個函式中,讓它不會在畫面轉譯時馬上被執行,寫法上可以這麼做:
這樣的話,畫面轉譯的時候 handleClick
就不會馬上被執行,而是在使用者點擊按鈕的時候才會去執行 () => handleClick('increment')
這個函式。
此部分完整的程式碼如下,或參考 Day 8 - Multiple Counters - event handlers with parameters @ CodePen:
const { useState } = React;
const Counter = () => {
const [count, setCount] = useState(5);
const handleClick = (type) => {
if (type === 'increment') {
setCount(count + 1);
}
if (type === 'decrement') {
setCount(count - 1);
}
};
return (
<div className="container">
<div
className="chevron chevron-up"
onClick={() => handleClick('increment')}
style={{
visibility: count >= 10 && 'hidden',
}}
/>
<div className="number">{count}</div>
<div
className="chevron chevron-down"
onClick={() => handleClick('decrement')}
style={{
visibility: count <= 0 && 'hidden',
}}
/>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(<Counter />);
這個寫法比較進階一點,如果你對於 JavaScript 還不是那麼熟悉的話,建議可以先跳過這段。
如果你覺得既然都已經把事件處理器抽成函式,卻又要在 onClick={}
的 {}
內多包一個函式很多餘的話,也可以把 handleClick
改成這樣,讓 handleClick
被執行的時候實際上是回傳一個已經帶有 type
的函式:
// handleClick('increment') 執行的時候實際上是回傳一個 type 為 increment 的函式
const handleClick = (type) => {
return function () {
if (type === 'increment') {
setCount(count + 1);
}
if (type === 'decrement') {
setCount(count - 1);
}
};
};
那麼在 onClick
中就可以寫像這樣:
return (
<div className="container">
<div
className="chevron chevron-up"
onClick={handleClick('increment')}
style={{
visibility: count >= 10 && 'hidden',
}}
/>
{/* ... */}
<div
className="chevron chevron-down"
onClick={handleClick('decrement')}
style={{
visibility: count <= 0 && 'hidden',
}}
/>
</div>
);
之所以可以這樣寫,是因為當畫面轉譯的時候,雖然 handleClick('increment')
會馬上被執行沒錯,但 handleClick('increment')
執行後並不是馬上去呼叫 setCount
方法,實際上是回傳了另一個 type
為 increment
的函式到 {}
內。這個被回傳的函式一樣會在按鈕的點擊事件被促發時被呼叫到。
上面提到的 handleClick
方法,由於是一個函式直接回傳另一個函式,因此在箭頭函式中,甚至可以精簡成這樣:
const handleClick = (type) => () => {
if (type === 'increment') {
setCount(count + 1);
}
if (type === 'decrement') {
setCount(count - 1);
}
};
意思一樣是當 handleClick
執行後會回傳 (type) =>
後面 () => { ... }
的這個函式。
如果你覺得都是 setCount()
只是一個是 + 1
,另一個是 - 1
,甚至可以再精簡成這樣:
const handleClick = (type) => () =>
setCount(type === 'increment' ? count + 1 : count - 1);
handleClick
被呼叫的時候,會回傳一個函式,這個函式才是事件處理器,而這個事件處理器也只是根據 type
的不同去執行 setCount
。
如果你覺得一下還沒辦法接受這種寫法,看得頭昏腦脹的話,你可以先把這整段略過,等之後越來越熟練後,就會比較容易理解了。完整的程式碼如下,也可以參考 Day 8 - Multiple Counters - event handlers with parameters (Advanced) @ CodePen:
上面把程式碼整理完成後,我們就用剛剛上面第一個完成的程式碼 Day 8 - Multiple Counters - clean code with event handlers 來繼續說明。
其實作法不難,因為 React 中每個元件其實都是各自獨立的,因此當我們想要一次產出非常多的計數器時,只需要寫很多次 <Counter />
,讓我們先產生 7 個計數器就好,像下面這樣:
const Counter = () => {
// ...
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div>
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
</div>
);
提醒:記得因為一個 JSX 元素最多只能有一個最外層的元素,因此當我們要轉譯很多的
<Counter />
時,為了要讓外層只有一個元素,可以加上一個額外的<div>
把所有<Counter />
包起來。
同時一併在最外層的 <div>
透過行內樣式(inline-style)添加 CSS 樣式,像是這樣:
// ...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
<Counter />
</div>
);
同時在 CSS 程式編輯區的 .container
中,把每個計數器透過 min-width
設定最小寬度:
.container {
display: flex;
align-items: center;
flex-direction: column;
min-width: 200px;
}
這時候你可以試著玩玩看,應該會發現每個計數器都是獨立的,彼此的數字不會互相干擾。沒錯,元件就是這麼方便:
你可能會說,既然 JSX 本質上都是 JavaScript 了,難道還得要手動複製貼上 <Counter />
,不能用迴圈的方式,看要幾個有幾個嗎?
當然是可以的!既然 JSX 本質上就是 JavaScript ,那麼你當然可以使用 JavaScript 學到的方式來重複產生多個計數器。
當在 JavaScript 中要重複執行某一個內容或動作時,很直覺的會想到可以用 for 迴圈
。首先你可能會很直覺的這麼寫:
// ❌ 錯誤寫法:if 不是 expressions 不能直接放在 JSX 的 {} 內
// ...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
{
for (let i = 0; i < 10; i ++) {
<Counter />
}
}
</div>
);
但這麼做程式並沒有辦法正確執行,原因在 for 迴圈
本身是個 statements 而非 expressions,執行的時候並不會有回傳值,因此不能直接放到 JSX 中的 {}
內去執行。那麼實際上可以怎麼做呢?
在 React 中,當我們要做重複轉譯多個元件時,最常使用到的是透過陣列的 map
方法,因為 map
這個方法會有回傳值,所以可以直接在 JSX 中使用。
實際的做法會像這樣:
Array.from()
先建立一個帶有 n 個元素的陣列map
方法,並且每次都回傳 <Counter />
元素在建立帶有多個元素的陣列時,經常會使用到 Array.from()
這個方法,下面列出常用的方式:
// 產生元素數目為 10,元素值都為 undefined 的陣列
Array.from({ length: 10 }); // [undefined, undefined, ..., undefined]
// 產生元素數目為 10,元素值為 0 ~ 9 的陣列
Array.from({ length: 10 }, (_, index) => index); // [0, 1, 2, ..., 8, 9]
因此透過 Array.from()
我們可以先建立一個帶有 n 個元素的陣列,並取名為 counters
:
// STEP 1: 建立元素數目為 14,內容為 [0, 1, ..., 13]
const counters = Array.from({ length: 14 }, (_, index) => index);
Array.from()
詳細的用法可以參考 MDN 的說明
接下來,就可以在 JSX 中透過在 {}
內使用 counters.map
的方式,就可以產生帶有多個 <Counter />
的陣列,像是這樣:
// ...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
{/* STEP 2: 使用 map 產生多個 <Counter /> */}
{counters.map((item) => (
<Counter />
))}
</div>
);
實際上 counters.map
會產生帶有許多 <Counter />
元件的陣列,而 JSX 在解析的時候,會就會把這個陣列中的 React 元件轉譯出來:
// ...
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
{/* STEP 2: 使用 map 產生多個 <Counter /> */}
{[
<Counter />,
<Counter />,
<Counter />,
//...
]}
</div>
);
現在,你只需要更改 Array.from({ length: n })
中 n
的數目,就可以根據你的需要產生不同數量的計數器。只要你的電腦夠強,你要一次打 100 個也沒問題拉!
完整的程式碼如下,或參考 Day 8 - Multiple Counters Finished @ CodePen:
const { useState } = React;
// STEP 1: 建立元素數目為 14,內容為 [0, 1, ..., 13]
const counters = Array.from({ length: 14 }, (_, index) => index);
const Counter = () => {
const [count, setCount] = useState(5);
const handleIncrement = () => setCount(count + 1);
const handleDecrement = () => setCount(count - 1);
return (
<div className="container">
<div
className="chevron chevron-up"
onClick={handleIncrement}
style={{
visibility: count >= 10 && 'hidden',
}}
/>
<div className="number">{count}</div>
<div
className="chevron chevron-down"
onClick={handleDecrement}
style={{
visibility: count <= 0 && 'hidden',
}}
/>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
{/* STEP 2: 使用 map 產生多個 <Counter /> */}
{counters.map((item) => (
<Counter />
))}
</div>
);
看到這種一排數字的計數器就會想要排列成 "87878787"
(對不起我去面壁思過......
你的做法非常正確??
我覺得 "55665566" 也是不錯的選擇XDD
56不能亡 (拭淚
想請教一個問題,這裏的Count應該不能共用這個count變量的吧?只是為了舉例這樣寫的吧?
你好,不太懂你的意思,能否多說明一些呢?
[...Array(5).keys()]
上面這個好像比下面簡單
Array.from({ length: 10 }, (_, index) => index)
真的耶!學習了!非常感謝!!
為什麼要給 array 數字呢? 全部是空的也可以 map 吧
Array.from({ length: 10 }, (_, index) => index)
只要 Array.from({ length: 10 }) 是不是就可以了。
或是 [...Array(10)]
flowblue 上面有人留言提到了喔~
好像new Array(10);
也可以的樣子..?!
反正陣列裡面是什麼不重要~
更:實測不行XD,new Array(10);
裡面是empty (即使各別看元素[0],[1]...是寫undefined)